{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "assigned-excuse",
   "metadata": {},
   "source": [
    "# Using ConstControl to control other controllers\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "colonial-flower",
   "metadata": {},
   "source": [
    "This tutorial shows how to use ConstControl to control other controllers in pandapower. In this example we set an attribute (a setpoint voltage) to the transformer tap changer controller."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "several-yugoslavia",
   "metadata": {},
   "source": [
    "First we need some imports. Specific for this example are:\n",
    "\n",
    "\n",
    "* control -> Trafo Controller that uses local tap changer to control the voltage\n",
    "* ConstControl -> \"constant\" controllers to update the voltage\n",
    "* DFData -> The Dataframe Datasource. This Dataframe holds the time series to be calculated\n",
    "* OutputWriter -> The output writer, which is required to write the outputs to the hard disk\n",
    "* run_timeseries -> the \"main\" time series function, which basically calls the controller functions (to update the P, Q of the ConstControllers) and runpp."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "nuclear-yemen",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-20T13:13:27.906069Z",
     "start_time": "2025-10-20T13:13:23.549374Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "from pandapower.create import (\n",
    "    create_empty_network,\n",
    "    create_bus,\n",
    "    create_ext_grid,\n",
    "    create_line,\n",
    "    create_load,\n",
    "    create_transformer,\n",
    "    create_sgen\n",
    ")\n",
    "from pandapower.run import set_user_pf_options\n",
    "from pandapower.timeseries import DFData, OutputWriter, run_timeseries\n",
    "from pandapower.control import ConstControl\n",
    "import pandapower.control as control\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "rng = np.random.default_rng(0)\n",
    "%matplotlib inline "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "collectible-spice",
   "metadata": {},
   "source": [
    "First we look at the time series example function. It follows these steps:\n",
    "\n",
    "1. create the datasource (which contains the time series P values)\n",
    "2. create the controllers to update the voltage\n",
    "3. define the output writer and desired variables to be saved in the output object directory\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "informative-aside",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-20T13:13:27.922109Z",
     "start_time": "2025-10-20T13:13:27.912078Z"
    }
   },
   "outputs": [],
   "source": [
    "def timeseries_example(net, output_dir):\n",
    "    # 1. create (random) data source\n",
    "    n_timesteps = 24\n",
    "    profiles, ds = create_data_source(n_timesteps)\n",
    "    # 2. create controllers \n",
    "    create_controllers(net, ds)\n",
    "    # time steps to be calculated. Could also be a list with non-consecutive time steps\n",
    "    time_steps = range(0, n_timesteps)\n",
    "    # 3. the output writer with the desired results to be stored to files.\n",
    "    ow = create_output_writer(net, time_steps, output_dir=output_dir)\n",
    "    return ow"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "unlikely-equilibrium",
   "metadata": {},
   "source": [
    "We start by creating a simple example pandapower net consisting of five buses, a transformer, three lines, a load and a sgen. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "restricted-stupid",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-20T13:13:27.938060Z",
     "start_time": "2025-10-20T13:13:27.928961Z"
    }
   },
   "outputs": [],
   "source": [
    "def simple_test_net():\n",
    "    \"\"\"\n",
    "    simple net that looks like:\n",
    "\n",
    "    ext_grid b0---b1 trafo(110/20) b2----b3 load\n",
    "                                    |\n",
    "                                    |\n",
    "                                    b4 sgen\n",
    "    \"\"\"\n",
    "    net= create_empty_network()\n",
    "    set_user_pf_options(net, init_vm_pu=\"flat\", init_va_degree=\"dc\", calculate_voltage_angles=True)\n",
    "\n",
    "    b0 = create_bus(net, 110)\n",
    "    b1 = create_bus(net, 110)\n",
    "    b2 = create_bus(net, 20)\n",
    "    b3 = create_bus(net, 20)\n",
    "    b4 = create_bus(net, 20)\n",
    "\n",
    "    create_ext_grid(net, b0)\n",
    "    create_line(net, b0, b1, 10, \"149-AL1/24-ST1A 110.0\")\n",
    "    create_transformer(net, b1, b2, \"25 MVA 110/20 kV\", name='tr1')\n",
    "    create_line(net, b2, b3, 10, \"184-AL1/30-ST1A 20.0\")\n",
    "    create_line(net, b2, b4, 10, \"184-AL1/30-ST1A 20.0\")\n",
    "\n",
    "    create_load(net, b2, p_mw=20., q_mvar=10., name='load1')\n",
    "    create_sgen(net, b4, p_mw=20., q_mvar=0.15, name='sgen1')\n",
    "\n",
    "    return net"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "indian-grant",
   "metadata": {},
   "source": [
    "The data source is a simple pandas DataFrame. It contains random values for the load and the sgen P values (\"profiles\") and values within a range between 0.95 and 1.05 for the voltage setpoint.\n",
    "Note that the profiles are identified by their column name (\"load1_p\", \"sgen1_p\" , \"c\"). \n",
    "The DFData(profiles) converts the DataFrame to the required format for the controllers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dirty-struggle",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-20T13:13:27.953395Z",
     "start_time": "2025-10-20T13:13:27.944536Z"
    }
   },
   "outputs": [],
   "source": [
    "def create_data_source(n_timesteps=24):\n",
    "    profiles = pd.DataFrame()\n",
    "    profiles['load1_p'] = rng.random(n_timesteps) * 20.\n",
    "    profiles['sgen1_p'] = rng.random(n_timesteps) * 20.\n",
    "    profiles['c'] = 0.95 + rng.random(n_timesteps) * (1.05-0.95)\n",
    "\n",
    "    ds = DFData(profiles)\n",
    "    \n",
    "    # a + r*k\n",
    "    # k = b-a\n",
    "    # range between a and b within k\n",
    "\n",
    "    return profiles, ds"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "disciplinary-apple",
   "metadata": {},
   "source": [
    "First we created a transformer tap changer contoller c of class ContinuousTapControl and then use the ConstControl to set attribute (a voltage setpoint) of the tap changer controller. An attribute of an object can be set if it is specified as \"object.attribute\" (e.g. \"object.vm_set_pu\")."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fixed-interstate",
   "metadata": {},
   "source": [
    "The parameters element_index and profile name should match. This way, it should be clear which profile belongs to which element. In this case we map:\n",
    "* first controller in dataframe (element_index=[0]) to the profile_name \"c\"\n",
    "* first sgen in dataframe (element_index=[0]) to the profile_name \"sgen1_p\"\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "structured-porcelain",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-20T13:13:27.969011Z",
     "start_time": "2025-10-20T13:13:27.959914Z"
    }
   },
   "outputs": [],
   "source": [
    "def create_controllers(net, ds):\n",
    "    c = control.ContinuousTapControl(net=net, element_index=0, vm_set_pu=0.98, tol=1e-6)\n",
    "    ConstControl(net, element='controller', variable='object.vm_set_pu', element_index=c.index, data_source=ds, \n",
    "                 profile_name='c')\n",
    "    ConstControl(net, element='sgen', variable='p_mw', element_index=0, data_source=ds, profile_name='sgen1_p')\n",
    "    ConstControl(net, element='load', variable='p_mw', element_index=0, data_source=ds, profile_name='load1_p')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fatal-shift",
   "metadata": {},
   "source": [
    "create the output writer and desired variables to be saved in the attribute \"output\" of the output writer.\n",
    "Instead of saving the whole net (which takes a lot of time), we extract only predefined outputs.\n",
    "In this case we:\n",
    "* save the results to \"output\" attribute of the output writer object\n",
    "* log the variables \"p_mw\" from \"res_load\" and \"res_sgen\", \"vm_pu\" from \"res_bus\" and two res_line variables."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "secret-family",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-20T13:13:27.984547Z",
     "start_time": "2025-10-20T13:13:27.977045Z"
    }
   },
   "outputs": [],
   "source": [
    "def create_output_writer(net, time_steps, output_dir):\n",
    "    ow = OutputWriter(net, time_steps, output_path=output_dir, output_file_type=\".xls\", log_variables=[])\n",
    "    # these variables are saved to the harddisk after / during the time series loop\n",
    "    ow.log_variable('res_load', 'p_mw')\n",
    "    ow.log_variable('res_sgen', 'p_mw')\n",
    "    ow.log_variable('res_bus', 'vm_pu')\n",
    "    ow.log_variable('res_line', 'loading_percent')\n",
    "    ow.log_variable('res_line', 'i_ka')\n",
    "    return ow"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "national-desperate",
   "metadata": {},
   "source": [
    "Here we call the main time series function to calculate the desired results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fifteen-imaging",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-20T13:13:34.601926Z",
     "start_time": "2025-10-20T13:13:27.992064Z"
    }
   },
   "outputs": [],
   "source": [
    "net = simple_test_net()\n",
    "\n",
    "ow = timeseries_example(net, None)\n",
    "run_timeseries(net, time_steps=range(24))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "automatic-sequence",
   "metadata": {},
   "source": [
    "## Plot results\n",
    "Now let us plot the results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "legal-clerk",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-20T13:13:34.617822Z",
     "start_time": "2025-10-20T13:13:34.608998Z"
    }
   },
   "outputs": [],
   "source": [
    "def plot_results(ow, variable, title):\n",
    "    p= ow.output[variable]\n",
    "    p.plot(label=variable)\n",
    "    plt.xlabel(\"time step\")\n",
    "    plt.ylabel(variable)\n",
    "    plt.title(title)\n",
    "    plt.grid()\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "toxic-wrong",
   "metadata": {},
   "source": [
    "Now let us compare the setpoint for voltage and the resulting voltage. To this end, we access the results of the time series simulation directly from the output writer object (attribute \"output\"):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "compressed-familiar",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-20T13:13:34.898366Z",
     "start_time": "2025-10-20T13:13:34.624888Z"
    }
   },
   "outputs": [],
   "source": [
    "ax=net.controller.at[1,'object'].data_source.df['c'].plot(zorder=1, linestyle='--', label='Voltage setpoint (p.u.)')\n",
    "ow.output['res_bus.vm_pu'][net.trafo.at[0, 'lv_bus']].plot(ax=ax, zorder=0, label='Voltage at the controlled bus (p.u.)')\n",
    "ax.legend(loc='upper center')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "static-neighborhood",
   "metadata": {},
   "source": [
    "As we can see, voltage setpoint of the transformer tap changer controller has been adjusted according to the values in the ConstControl.\n",
    "Now, we can plot all the results of the time seriues simulation:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "lyric-coating",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-10-20T13:13:35.576144Z",
     "start_time": "2025-10-20T13:13:34.916291Z"
    }
   },
   "outputs": [],
   "source": [
    "plot_results(ow, 'res_bus.vm_pu', 'Voltage (p.u.)')\n",
    "plot_results(ow, 'res_line.loading_percent', 'Line Loading')\n",
    "plot_results(ow, 'res_load.p_mw', 'Real Power at load')\n",
    "plot_results(ow, 'res_sgen.p_mw', 'Real Power at sgen')"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
